package edu.washington.cs.workflow.instrument;

import java.io.Writer;

import com.ibm.wala.shrikeBT.ConditionalBranchInstruction;
import com.ibm.wala.shrikeBT.ConstantInstruction;
import com.ibm.wala.shrikeBT.Constants;
import com.ibm.wala.shrikeBT.Disassembler;
import com.ibm.wala.shrikeBT.DupInstruction;
import com.ibm.wala.shrikeBT.GetInstruction;
import com.ibm.wala.shrikeBT.IInstruction;
import com.ibm.wala.shrikeBT.Instruction;
import com.ibm.wala.shrikeBT.InvokeInstruction;
import com.ibm.wala.shrikeBT.LoadInstruction;
import com.ibm.wala.shrikeBT.MethodData;
import com.ibm.wala.shrikeBT.MethodEditor;
import com.ibm.wala.shrikeBT.ReturnInstruction;
import com.ibm.wala.shrikeBT.SwapInstruction;
import com.ibm.wala.shrikeBT.ThrowInstruction;
import com.ibm.wala.shrikeBT.Util;
import com.ibm.wala.shrikeBT.MethodEditor.Output;
import com.ibm.wala.shrikeBT.analysis.Verifier;
import com.ibm.wala.shrikeBT.shrikeCT.CTDecoder;
import com.ibm.wala.shrikeBT.shrikeCT.ClassInstrumenter;
import com.ibm.wala.shrikeCT.ClassReader;
import com.ibm.wala.shrikeCT.ClassWriter;

import edu.washington.cs.workflow.util.Globals;
import edu.washington.cs.workflow.util.Utils;

/**
 * Instrument 2 parts. The first is method entry/exit, or value returning method
 * invocation calls
 * */
public class WorkflowTraceInstrumenter extends AbstractInstrumenter {
	
	static final String fieldName = "exec_tracer_enabled";
	
	static final String tracerFieldName = "tracer";
	static final String traceMethodEntry = "traceMethodEntry";
	static final String traceMethodExit = "traceMethodExit";
	static final String traceMethodExcepExit = "traceMethodExcepExit";
	static final String traceValueReturn = "traceValueReturning";
	static final String traceObject = "traceObject";
	
	static final Instruction getTracer = Util.makeGet(ExecTracer.class, tracerFieldName);
	
	static final Instruction recordEntry = Util.makeInvoke(ExecTracer.class, traceMethodEntry, new Class[] { String.class, Object.class });
	static final Instruction recordExit = Util.makeInvoke(ExecTracer.class, traceMethodExit, new Class[] { String.class, Object.class });
	static final Instruction recordExcepExit = Util.makeInvoke(ExecTracer.class, traceMethodExcepExit, new Class[] { String.class, Object.class });
	
	static final Instruction recordValueReturn = Util.makeInvoke(ExecTracer.class, traceValueReturn, new Class[] { String.class, Object.class });
	
	public final InstruPointsController controller; 
	
	public WorkflowTraceInstrumenter(InstruPointsController controller) {
		Utils.checkNotNull(controller);
		this.controller = controller;
	}
	
	@Override
	protected void doClass(ClassInstrumenter ci, Writer w) throws Exception {
		final String className = ci.getReader().getName();
	    w.write("Class: " + className + "\n");
	    w.flush();
	    
	    for (int m = 0; m < ci.getReader().getMethodCount(); m++) {
	      MethodData d = ci.visitMethod(m);
	      // d could be null, e.g., if the method is abstract or native
	      if (d != null) {
	        w.write("Instrumenting " + ci.getReader().getMethodName(m) + " " + ci.getReader().getMethodType(m) + ":\n");
	        w.flush();
	        if (disasm) {
	          w.write("Initial ShrikeBT code:\n");
	          (new Disassembler(d)).disassembleTo(w);
	          w.flush();
	        }
	        if (verify) {
	          Verifier v = new Verifier(d);
	          v.verify();
	        }

	        MethodEditor me = new MethodEditor(d);
	        me.beginPass();

	        //the unique method signature
        	final String methodSig = this.getMethodSignature(d);
        	
        	//XXX no handling of parameters yet, no static method yet
        	//need to take a look
        	if(this.controller.isEventHandlingMethod(d)) {
        	  me.insertAtStart(new MethodEditor.Patch() {
	            @Override
	            public void emitTo(MethodEditor.Output w) {
	              Utils.debugPrintln("the beginning of: " + methodSig);
                  w.emit(getTracer);
                  w.emit(ConstantInstruction.makeString(methodSig));
//                  w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
                  
                  w.emit(LoadInstruction.make(Constants.TYPE_Object, 1)); //load the first argument
                  
                  w.emit(recordEntry);
                  //keep the statistics
                  InstrumentStats.addInsertedInstructions(1);
	            }
	          });
        	
        	  IInstruction[] instr = me.getInstructions();
              for (int i = 0; i < instr.length; i++) {
                if (instr[i] instanceof ReturnInstruction) {
                  me.insertBefore(i, new MethodEditor.Patch() {
                    @Override
                    public void emitTo(MethodEditor.Output w) {
                      Utils.debugPrintln("the end of: " + methodSig);
                      w.emit(getTracer);
                      w.emit(ConstantInstruction.makeString(methodSig));
//                      w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
                      w.emit(LoadInstruction.make(Constants.TYPE_Object, 1)); //load this
                      w.emit(recordExit);
                      //keep the statistics
                      InstrumentStats.addInsertedInstructions(1);
                    }
                  });
                }
              }
              
              me.addMethodExceptionHandler(null, new MethodEditor.Patch() {
                @Override
                public void emitTo(Output w) {
                	Utils.debugPrintln("the exception-throwing end of: " + methodSig);
                    w.emit(getTracer);
                    w.emit(ConstantInstruction.makeString(methodSig));
//                    w.emit(LoadInstruction.make(Constants.TYPE_Object, 0)); //load this
                    w.emit(LoadInstruction.make(Constants.TYPE_Object, 1)); //load this
                    w.emit(recordExcepExit);
                    //keep the statistics
                    InstrumentStats.addInsertedInstructions(1);
                    //must rethrow the exception
                    w.emit(ThrowInstruction.make(false));
                }
              });
        	}
        	
        	//instrument the value-returning method of a gui object
        	IInstruction[] instr = me.getInstructions();
            for (int i = 0; i < instr.length; i++) {
              if (instr[i] instanceof InvokeInstruction) {
            	final InvokeInstruction invokeInstr = (InvokeInstruction)instr[i];
            	if(this.controller.isGUIValueReturningMethod(invokeInstr)) {
            		me.insertBefore(i, new MethodEditor.Patch() {
                        @Override
                        public void emitTo(MethodEditor.Output w) {
                          w.emit(DupInstruction.make(0));
                          w.emit(getTracer);
                          w.emit(SwapInstruction.make());
                          w.emit(ConstantInstruction.makeString(
                        		  invokeInstr.getClassType() + " " +
                        		  invokeInstr.getMethodName() + " " +
                        		  invokeInstr.getPoppedCount() + " " +
                        		  invokeInstr.getMethodSignature()));
                          w.emit(SwapInstruction.make());
                          //w.emit(LoadInstruction.make(Constants.TYPE_Object, 0));
                          
                          w.emit(recordValueReturn);
                          InstrumentStats.addInsertedInstructions(1);
                        }
                      });
            	}
              }
            }
	        
	        // this updates the data, instrumentation ends
	        me.applyPatches();
	        if (disasm) {
	          w.write("Final ShrikeBT code:\n");
	          (new Disassembler(d)).disassembleTo(w);
	          w.write(Globals.lineSep);
	          w.flush();
	        }
	      }
	    }

	    //add a class field
	    if (ci.isChanged()) {
	      ClassWriter cw = ci.emitClass();
	      cw.addField(ClassReader.ACC_PUBLIC | ClassReader.ACC_STATIC, fieldName, Constants.TYPE_boolean, new ClassWriter.Element[0]);
	      instrumenter.outputModifiedClass(ci, cw);
	    }
	}
}